﻿//-----------------------------------------------------------------------
// <copyright file="SQLiteCommand.cs" company="NextLine Software">
//     Copyright (c) NextLine Software. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace NextLine.Data.SQLite
{
    using System;
    using System.Collections.Generic;
    using System.Data;
    using NextLine.Data.SQLite.Wrapper;

    public partial class SQLiteCommand : IDbCommand
    {
        private static readonly IDictionary<Type, int> sqliteTypeMapping = InitializeSQLiteTypeMapping();
        private readonly IDataParameterCollection parameters = new SQLiteDataParameterCollection();

        private int commandTimeout = 30;
        private SQLiteConnection connection;
        private Sqlite3.Vdbe preparedCommand;
        private string commandText;

        public SQLiteCommand()
        {
        }

        public SQLiteCommand(string commandText)
        {
            this.CommandText = commandText;
        }

        public SQLiteCommand(string commandText, SQLiteConnection connection)
        {
            if (connection == null)
            {
                throw new ArgumentNullException("connection");
            }

            this.Connection = connection;
            this.CommandText = commandText;
        }

        public string CommandText
        {
            get
            {
                return this.commandText;
            }

            set
            {
                if (!StringComparer.OrdinalIgnoreCase.Equals(this.commandText, value))
                {
                    this.CleanupPreparedStatement();
                    this.commandText = value;
                }
            }
        }

        public int CommandTimeout
        {
            get
            {
                return this.commandTimeout;
            }

            set
            {
                if (value < 0)
                {
                    throw new ArgumentException("CommandTimeout must be a positive value.");
                }

                this.commandTimeout = value;
            }
        }

        public CommandType CommandType
        {
            get
            {
                return CommandType.Text;
            }

            set
            {
                if (value != CommandType.Text)
                {
                    throw new NotSupportedException("Only Text commands are supported.");
                }
            }
        }

        public IDbConnection Connection
        {
            get
            {
                return this.connection;
            }

            set
            {
                if (this.connection != null)
                {
                    if (this.connection.State == ConnectionState.Executing)
                    {
                        throw new InvalidOperationException("Cannot update command connection which is currently executing.");
                    }
                }

                this.connection = (SQLiteConnection)value;
            }
        }

        public IDataParameterCollection Parameters
        {
            get
            {
                return this.parameters;
            }
        }

        public IDbTransaction Transaction
        {
            get
            {
                throw new NotImplementedException();
            }

            set
            {
                throw new NotImplementedException();
            }
        }

        public UpdateRowSource UpdatedRowSource
        {
            get
            {
                throw new NotSupportedException();
            }

            set
            {
                throw new NotSupportedException();
            }
        }

        public void Cancel()
        {
            throw new NotImplementedException();
        }

        public IDbDataParameter CreateParameter()
        {
            return new SQLiteDataParameter(null, null);
        }

        public int ExecuteNonQuery()
        {
            this.Prepare();
            this.BindParameters();

            var result = Sqlite3.sqlite3_step(this.preparedCommand);
            switch (result)
            {
                case Sqlite3.SQLITE_ERROR:
                    string msg = Sqlite3.sqlite3_errmsg(this.connection.DatabaseHandle);
                    this.CleanupPreparedStatement();
                    throw new SQLiteException(msg);

                case Sqlite3.SQLITE_DONE:
                    int rowsAffected = Sqlite3.sqlite3_changes(this.connection.DatabaseHandle);
                    Sqlite3.sqlite3_reset(this.preparedCommand);
                    return rowsAffected;

                case Sqlite3.SQLITE_CANTOPEN:
                    this.CleanupPreparedStatement();
                    throw new SQLiteException("Cannot open database file");

                case Sqlite3.SQLITE_CONSTRAINT:
                    string msgC = Sqlite3.sqlite3_errmsg(this.connection.DatabaseHandle);
                    this.CleanupPreparedStatement();
                    throw new SQLiteException(msgC);

                default:
                    this.CleanupPreparedStatement();
                    throw new SQLiteException("Unknown error");
            }
        }

        public IDataReader ExecuteReader()
        {
            return this.ExecuteReader(CommandBehavior.Default);
        }

        public IDataReader ExecuteReader(CommandBehavior behavior)
        {
            return new SQLiteDataReader(this);
        }

        public object ExecuteScalar()
        {
            // ExecuteReader and get value of first column of first row
            using (IDataReader reader = this.ExecuteReader())
            {
                object scalarResult = null;
                if (reader.Read() && reader.FieldCount > 0)
                {
                    scalarResult = reader.GetValue(0);
                }

                return scalarResult;
            }
        }

        public void Prepare()
        {
            if (this.preparedCommand != null)
            {
                Sqlite3.sqlite3_reset(this.preparedCommand);
                return;
            }

            if (this.connection == null || this.connection.State != ConnectionState.Open)
            {
                throw new InvalidOperationException("A valid open connection is required to prepare a command.");
            }

            // perform the prepare
            var sqliteCommand = new Sqlite3.Vdbe();
            if (Sqlite3.sqlite3_prepare_v2(this.connection.DatabaseHandle, this.CommandText, this.CommandText.Length, ref sqliteCommand, 0) != Sqlite3.SQLITE_OK)
            {
                throw new SQLiteException(Sqlite3.sqlite3_errmsg(this.connection.DatabaseHandle));
            }

            this.preparedCommand = sqliteCommand;
        }

        public void Dispose()
        {
            this.CleanupPreparedStatement();
        }

        private static IDictionary<Type, int> InitializeSQLiteTypeMapping()
        {
            IDictionary<Type, int> mappings = new Dictionary<Type, int>();
            mappings.Add(typeof(bool), Sqlite3.SQLITE_INTEGER);
            mappings.Add(typeof(ushort), Sqlite3.SQLITE_INTEGER);
            mappings.Add(typeof(short), Sqlite3.SQLITE_INTEGER);
            mappings.Add(typeof(uint), Sqlite3.SQLITE_INTEGER);
            mappings.Add(typeof(int), Sqlite3.SQLITE_INTEGER);
            mappings.Add(typeof(ulong), Sqlite3.SQLITE_INTEGER);
            mappings.Add(typeof(long), Sqlite3.SQLITE_INTEGER);
            mappings.Add(typeof(byte), Sqlite3.SQLITE_INTEGER);
            mappings.Add(typeof(sbyte), Sqlite3.SQLITE_INTEGER);

            mappings.Add(typeof(float), Sqlite3.SQLITE_FLOAT);
            mappings.Add(typeof(double), Sqlite3.SQLITE_FLOAT);
            mappings.Add(typeof(decimal), Sqlite3.SQLITE_FLOAT);

            mappings.Add(typeof(byte[]), Sqlite3.SQLITE_BLOB);

            mappings.Add(typeof(string), Sqlite3.SQLITE_TEXT);

            return mappings;
        }

        private void BindParameters()
        {
            Sqlite3.sqlite3_clear_bindings(this.preparedCommand);
            if (this.parameters.Count == 0)
            {
                return;
            }

            int bindingIndex = 1;
            IList<Binding> bindings = new List<Binding>(this.parameters.Count);
            foreach (var parameter in this.parameters)
            {
                Binding binding = new Binding();
                binding.Name = parameter.ParameterName;
                binding.Value = parameter.Value;

                if (binding.Name != null)
                {
                    binding.Index = Sqlite3.sqlite3_bind_parameter_index(this.preparedCommand, binding.Name);
                }
                else
                {
                    binding.Index = bindingIndex++;
                }

                bindings.Add(binding);
            }

            foreach (var binding in bindings)
            {
                if (binding.Value == null)
                {
                    Sqlite3.sqlite3_bind_null(this.preparedCommand, binding.Index);
                }
                else
                {
                    int type = 0;
                    if (!sqliteTypeMapping.TryGetValue(binding.Value.GetType(), out type))
                    {
                        throw new InvalidOperationException("Could not bind type: " + binding.Value.GetType());
                    }

                    switch (type)
                    {
                        case Sqlite3.SQLITE_INTEGER:
                            Sqlite3.sqlite3_bind_int64(this.preparedCommand, binding.Index, Convert.ToInt64(binding.Value));
                            break;

                        case Sqlite3.SQLITE_FLOAT:
                            Sqlite3.sqlite3_bind_double(this.preparedCommand, binding.Index, Convert.ToDouble(binding.Value));
                            break;

                        case Sqlite3.SQLITE_BLOB:
                            byte[] data = (byte[])binding.Value;
                            Sqlite3.sqlite3_bind_blob(this.preparedCommand, binding.Index, data, data.Length, null);
                            break;

                        case Sqlite3.SQLITE_TEXT:
                            Sqlite3.sqlite3_bind_text(this.preparedCommand, binding.Index, binding.Value.ToString(), -1, null);
                            break;
                    }
                }
            }
        }

        private void CleanupPreparedStatement()
        {
            if (this.preparedCommand != null)
            {
                Sqlite3.sqlite3_finalize(ref this.preparedCommand);
                this.preparedCommand = null;
            }
        }

        private struct Binding
        {
            public string Name;
            public object Value;
            public int Index;
        }
    }
}
